Bokeh property editor

In [1]:
import numpy as np
import panel as pn

pn.extension()

Bokeh's property system defines the valid properties for all the different Bokeh models. Using jslink we can easily tie a widget value to Bokeh properties on another widget or plot. This example defines functions that generate a property editor for the most common Bokeh properties. First, we define two functions that generate a set of widgets linked to a plot:

In [2]:
from bokeh.core.enums import LineDash, LineCap, MarkerType, NamedColor
from bokeh.models.plots import Model, _list_attr_splat

def meta_widgets(model):
    tabs = pn.Tabs(width=500)
    widgets = get_widgets(model)
    if widgets:
        tabs.append((type(model).__name__, widgets))
    for p, v in model.properties_with_values().items():
        if isinstance(v, _list_attr_splat):
            v = v[0]
        if isinstance(v, Model):
            subtabs = meta_widgets(v)
            if subtabs is not None:
                tabs.append((p.title(), subtabs))
                
    if hasattr(model, 'renderers'):
        for r in model.renderers:
            tabs.append((type(r).__name__, meta_widgets(r)))
    if hasattr(model, 'axis'):
        for pre, axis in zip('XY', model.axis):
            tabs.append(('%s-Axis' % pre, meta_widgets(axis)))
    if hasattr(model, 'grid'):
        for pre, grid in zip('XY', model.grid):
            tabs.append(('%s-Grid' % pre, meta_widgets(grid)))
    if not widgets and not len(tabs) > 1:
        return None
    elif not len(tabs) > 1:
        return tabs[0]
    return tabs
        
def get_widgets(model, skip_none=True, **kwargs):
    widgets = []
    for p, v in model.properties_with_values().items():
        if isinstance(v, dict):
            if 'value' in v:
                v = v.get('value')
            else:
                continue
        if v is None and skip_none:
            continue
                
        ps = dict(name=p, value=v, **kwargs)
        if 'alpha' in p:
            w = pn.widgets.FloatSlider(start=0, end=1, **ps)
        elif 'color' in p:
            if v in list(NamedColor):
                w = pn.widgets.Select(options=list(NamedColor), **ps)
            else:
                w = pn.widgets.ColorPicker(**ps)
        elif p.endswith('width'):
            w = pn.widgets.FloatSlider(start=0, end=20, **ps)
        elif 'marker' in p:
            w = pn.widgets.Select(name=p, options=list(MarkerType), value=v)
        elif p.endswith('cap'):
            w = pn.widgets.Select(name=p, options=list(LineCap), value=v)
        elif p == 'size':
            w = pn.widgets.FloatSlider(start=0, end=20, **ps)
        elif p.endswith('text') or p.endswith('label'):
            w = pn.widgets.TextInput(**ps)
        elif p.endswith('dash'):
            patterns = list(LineDash)
            w = pn.widgets.Select(name=p, options=patterns, value=v or patterns[0])
        else:
            continue
        w.jslink(model, value=p)
        widgets.append(w)
    return pn.Column(*sorted(widgets, key=lambda w: w.name))

Having defined these helper functions we can now declare a plot and use the meta_widgets function to generate the GUI:

In [3]:
from bokeh.plotting import figure

p = figure(title='This is a title', x_axis_label='x-axis', y_axis_label='y-axis')
xs = np.linspace(0, 10)
r = p.scatter(xs, np.sin(xs))

pn.Row(p, meta_widgets(p))
Out[3]:

Download this notebook from GitHub (right-click to download).